Search code examples
laravelemaillaravel-5email-threading

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


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.


Solution

  • 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. :)