Skip to main content
ElCoco uses Laravel’s mail system to send three transactional emails across two flows: quote submission and admin reply.

Email flows

Flow 1: Quote submission

When a client submits a quote via POST /api/quotes/submit, two emails are sent by QuoteApiController::submitQuote():
1

Client confirmation (QuoteReceived)

QuoteReceived is sent to quote->client_email with the subject "Cotización recibida - {reference}" and the generated PDF attached.
2

Admin notification (QuoteAdminNotification)

QuoteAdminNotification is sent to config('mail.admin_email') with the subject "📋 Nueva cotización recibida: {reference}" and a Markdown summary of the quote.
The admin notification recipient is read from config('mail.admin_email'). Set this key in config/mail.php or via an environment variable to ensure the DMI team receives submission alerts.

Flow 2: Admin reply (meeting scheduling)

1

Admin schedules a meeting

In the admin panel, the DMI team opens a quote at /admin/cotizaciones/{id} and submits the reply form with a meeting_date value (datetime string).
2

Reply is recorded

A QuoteReply record is created with the formatted meeting message stored in message and the scheduled datetime in sent_at.
3

PDF is regenerated

The quote’s PDF is rebuilt from the stored Quote and QuoteItem data and written to storage/app/public/quotes/{reference}.pdf.
4

Email is dispatched

QuoteReplyMail is sent to quote->client_email. The email includes the meeting message and the PDF as an attachment.

Route

POST /admin/cotizaciones/{quote}/reply
This route is protected by auth and verified middleware. The only required request field is meeting_date (validated as a date).

The meeting message format

The message text is generated by QuoteController::formatMeetingMessage():
private function formatMeetingMessage(string $meetingDate): string
{
    $date = new \DateTime($meetingDate);
    $formattedDate = $date->format('d/m/Y H:i');
    return "Cita Virtual en GoogleMeet: $formattedDate";
}
For example, an input of 2026-03-20 14:30:00 produces:
Cita Virtual en GoogleMeet: 20/03/2026 14:30
This string is passed to QuoteReplyMail and rendered in the email view at resources/views/emails/quote-reply.blade.php.

Mailables reference

QuoteReceived

App\Mail\QuoteReceived is sent to the client immediately after they submit a quote. It uses a Markdown template and attaches the generated PDF.
// Subject: "Cotización recibida - {reference}"
// Template: emails.quote.received (Markdown)
// To: quote->client_email
// Attachment: public/{pdf_path} → "cotizacion.pdf"
PropertyValue
SubjectCotización recibida - {reference}
Toquote->client_email
Viewemails.quote.received (Markdown)
Attachmentcotizacion.pdf (from storage/app/public/{pdf_path})

QuoteAdminNotification

App\Mail\QuoteAdminNotification is sent to the DMI team when a new quote arrives. It uses a Markdown template and includes quote summary data.
// Subject: "📋 Nueva cotización recibida: {reference}"
// Template: emails.quote.admin-notification (Markdown)
// To: config('mail.admin_email')
PropertyValue
Subject📋 Nueva cotización recibida: {reference}
Toconfig('mail.admin_email')
Viewemails.quote.admin-notification (Markdown)
Variables$quote, $blocks, $totalHours, $totalCost
MAIL_ADMIN_EMAIL is not defined in .env.example. You must add admin_email to config/mail.php and set a corresponding environment variable, or hardcode the value, before submission emails reach the DMI team.

The QuoteReplyMail mailable

App\Mail\QuoteReplyMail extends Laravel’s Mailable class and uses the Queueable and SerializesModels traits.
class QuoteReplyMail extends Mailable
{
    use Queueable, SerializesModels;

    public $quote;       // Quote model instance
    public $messageText; // Formatted meeting string
    public $pdfPath;     // Relative path in the public disk

    public function build()
    {
        return $this->subject('Tu cotización y cita virtual')
                    ->view('emails.quote-reply')
                    ->with([
                        'quote'       => $this->quote,
                        'messageText' => $this->messageText,
                    ])
                    ->attach(
                        storage_path('app/public/' . $this->pdfPath),
                        [
                            'as'   => $this->quote->reference . '.pdf',
                            'mime' => 'application/pdf',
                        ]
                    );
    }
}
PropertyDescription
SubjectTu cotización y cita virtual (fixed)
Toquote->client_email
Viewemails.quote-reply
Attachment name{quote->reference}.pdf
Attachment pathstorage/app/public/{pdfPath}
QuoteReplyMail implements Queueable but is dispatched synchronously via Mail::to()->send(). To queue it, change send() to queue() and ensure a queue worker is running.

Configuring SMTP in .env

By default, ElCoco uses the log mailer, which writes emails to storage/logs/laravel.log instead of sending them. Change MAIL_MAILER to smtp for real delivery.
MAIL_MAILER=log
Emails appear in storage/logs/laravel.log. No SMTP server required.

All mail environment variables

VariableDefaultDescription
MAIL_MAILERlogTransport driver. Use smtp for real delivery
MAIL_SCHEMEnullTLS scheme: blank (STARTTLS), ssl, or tls
MAIL_HOST127.0.0.1SMTP server hostname
MAIL_PORT2525SMTP port (587 for STARTTLS, 465 for SSL)
MAIL_USERNAMEnullSMTP authentication username
MAIL_PASSWORDnullSMTP authentication password
MAIL_FROM_ADDRESS[email protected]Sender From: address
MAIL_FROM_NAME${APP_NAME}Sender From: display name

Test email route

A development-only route is registered in routes/web.php for quickly verifying your mail configuration:
GET /test-mail
Route::get('/test-mail', function () {
    Mail::raw('Test email funcionando', function ($msg) {
        $msg->to('[email protected]')
            ->subject('Test Laravel Mail');
    });
    return 'Correo enviado';
});
This route is unauthenticated and hardcodes a recipient address. Remove or guard it before deploying to production.

Supported mail transports

Laravel’s config/mail.php lists all available transports. You can switch between them without code changes:
Standard SMTP. Works with any SMTP-compatible provider (Mailgun, Postmark, SendGrid, Gmail, etc.).
Writes the email content to the Laravel log. Default for local development. Useful for inspecting email output without a mail server.
Amazon Simple Email Service. Requires AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and the aws/aws-sdk-php package.
Postmark transactional email. Requires the postmark/postmark-php package and a POSTMARK_TOKEN.
Resend email API. Requires the resend/resend-laravel package and a RESEND_KEY.
Tries smtp first; falls back to log if delivery fails. Configured in config/mail.php.

Build docs developers (and LLMs) love