Laravel 5.6: How-to send emails, that clients will show them as threads / conversations?

2.1k views Asked by At

My Laravel application has a ticket system included, which is sending email notifications.

All emails are built and sent like this one:

public function build()
{
    $email_from_name = "Support - " . config('app.name');
    $subject = "[" . $this->ticket->id . "] " . $this->ticket->subject . " - " . config('app.name');

    return $this->from('[email protected]', $email_from_name)
                    ->subject($subject)
                    ->markdown('emails.customer.ticket.comment_added')
                        ->with([
                            'nickname' => $this->user->nickname,
                            'ticket_id' => $this->ticket->id,
                            'ticket_subject' => $this->ticket->subject,
                            'ticket_text' => $this->ticket_comments->text,
                        ]);
}

Unfortunately, when I get multiple of these emails, no email client (Outlook, Thunderbird, Roundcube,...) shows these emails as thread / conversation. All clients show each email as "new email" thread / conversation.

What specifies, that some emails are one thread / conversation and some not? How can I tell my Laravel application, that these emails are one thread / conversation?

I thought, it just needs to be the same email subject, but it doesn't work.

2

There are 2 answers

4
checker284 On BEST ANSWER

Thanks to @Yeeooow for the information regarding the RFC 2822 standard: The References and In-Reply-To headers must be set in compliance with the RFC 2822 standard.

Based on these information, I've checked some other email threads / conversations, which I had. All of these used the mentioned References and In-Reply-To headers in the same way. With this information, I've started developing to archive the same result.

Due of the fact, that we need to reference to old emails, we need a table, where we can store the Message-ID of each sent email. I've created this table in my case:

// Table: ticket_message_ids
public function up()
{
    Schema::create('ticket_message_ids', function (Blueprint $table) {
        $table->increments('id');
        $table->integer('ticket_id')->unsigned();
        $table->integer('reference_id')->unsigned(); // Optional; You may remove it or make it ->nullable()
        $table->string('message_id');
        $table->timestamps();
    });
}

With this table, we're able to store the Message-ID of each sent email and can reference to which ticket it belongs to. This will help us later also to get only associated Message-IDs related to this ticket - otherwise, we'll mix up different ticket histories in the same email thread.

Into the reference_id field, you can optionally store the associated ID of the task:

  • Ticket text added
  • Ticket operation executed (eg. supporter, priority, title or status changed)

In your mailable (eg. app\Mail\TicketTextAdded.php), you can now add the code section $this->withSwiftMessage() {} into your build() function to capture the current Message-ID of this new email and reference to all other emails before as well as store the new Message-ID`:

public function build()
{
    $email_from_name = "Support - " . config('app.name');
    $subject = "[" . $this->ticket->id . "] " . $this->ticket->subject . " - " . config('app.name');

    $email = $this->from('[email protected]', $email_from_name)
                    ->subject($subject)
                    ->markdown('emails.customer.ticket.comment_added')
                        ->with([
                            'nickname' => $this->user->nickname,
                            'ticket_id' => $this->ticket->id,
                            'ticket_subject' => $this->ticket->subject,
                            'ticket_text' => $this->ticket_comments->text,
                        ]);

    // Access underlaying Swift message
    $this->withSwiftMessage(function ($swiftmessage) {
        // Get all Message-IDs associated to this specific ticket
        $message_ids = TicketMessageIds::where('ticket_id', '=', $this->ticket->id)->get();

        // Build RFC2822 conform 'References' header
        // Example: 'References: <[email protected]> <[email protected]>'
        $header_references = "";            
        foreach($message_ids as $message_id) {
            if(empty($header_references)) {
                $header_references = $message_id->message_id;
            } else {
                $header_references = $header_references . " " . $message_id->message_id;
            }
        }

        // Build RFC2822 conform 'In-Reply-To' header
        // Example: 'In-Reply-To: <[email protected]>'
        $header_in_reply_to = TicketMessageIds::where('ticket_id', '=', $this->ticket->id)->orderBy('id', 'DESC')->get(['message_id'])->first()->message_id;

        // Add required custom headers with above values
        $headers = $swiftmessage->getHeaders();
        // 'X-Mailer' header is not required for this purpose
        // This header sets only a name for the client, which sent this message (typical values: Outlook 2016, PHPMailer v6.0.5,...)
        $headers->addTextHeader('X-Mailer', config('app.name') . ' (' . config('app.url') . ')');
        if(!empty($header_references)) {
            $headers->addTextHeader('References', $header_references);
        }
        $headers->addTextHeader('In-Reply-To', $header_in_reply_to);

        TicketMessageIds::create([
            'ticket_id' => $this->ticket->id,
            'message_id' => '<'.$swiftmessage->getId().'>'
        ]);
    });

    return $email;
}

FYI: You could also change the Message-ID there, where we've set the custom headers, but this needs to comply to the relevant RFC documents:

$msgId = $swiftmessage->getHeaders()->get('Message-ID');
$msgId->setId(time() . '.' . uniqid('thing') . '@example.org');

Further information: https://swiftmailer.symfony.com/docs/headers.html#id-headers

Hopefully, I could help somebody else with these information. :)

3
alberto-bottarini On

Threads are created automatically by email clients. You couldn't do anything