Pterodactyl is a Laravel application, and its email behavior follows Laravel's mail and queue subsystems rather than PHP's native mail() function. When password resets, account verifications, or server-ready notifications silently vanish, the problem almost always sits in one of three places: the mail transport configuration, the queue worker that actually dispatches messages, or the DNS records that decide whether receiving servers trust your mail. On a managed shared account you cannot restart services or touch server daemons, but every one of these layers is reachable through the panel, the panel's own .env file, and a cron entry. Understanding how the pieces connect keeps you from chasing the wrong symptom.
How Pterodactyl decides to send mail
Pterodactyl reads its mail settings from the .env file in the panel root (commonly /home/youruser/panel/.env or wherever you installed it under public_html). The relevant keys are MAIL_DRIVER, MAIL_HOST, MAIL_PORT, MAIL_USERNAME, MAIL_PASSWORD, MAIL_ENCRYPTION, and MAIL_FROM_ADDRESS. Newer builds using Laravel 9+ may name the driver key MAIL_MAILER instead of MAIL_DRIVER; set both to be safe. Many installs ship with the driver left on a placeholder or on sendmail, and sendmail is frequently disabled or heavily rate-limited on shared LiteSpeed servers. That single mismatch explains a large share of "emails never arrive" tickets.
The correct transport on Hostiso shared hosting is authenticated SMTP pointing at your own mail server on the account. Create a dedicated mailbox first in cPanel under Email Accounts (or DirectAdmin under E-Mail Accounts), for example panel@yourdomain.com, and give it a strong password. Then edit .env through the cPanel File Manager. Open the panel directory, use Settings to enable Show Hidden Files (dotfiles), right-click .env, and choose Edit. A working configuration looks like this:
MAIL_MAILER=smtp
MAIL_DRIVER=smtp
MAIL_HOST=mail.yourdomain.com
MAIL_PORT=465
MAIL_USERNAME=panel@yourdomain.com
MAIL_PASSWORD=your-mailbox-password
MAIL_ENCRYPTION=ssl
MAIL_FROM_ADDRESS=panel@yourdomain.com
MAIL_FROM_NAME="Your Panel"
Use port 465 with ssl, or port 587 with tls. Avoid port 25, which is commonly blocked outbound on shared platforms. Point MAIL_HOST at your own domain's mail host, not a remote provider, unless you intentionally relay through an external service. The MAIL_FROM_ADDRESS must live on a domain you control and match the mailbox you authenticate with, because mismatched sender identities are the fastest route to a spam folder.
After any change to .env, Laravel's cached configuration must be cleared or it will keep reading old values. Pterodactyl exposes settings through the admin area, but the config cache is regenerated by artisan. If you have shell access through a cPanel Terminal feature, run php artisan config:clear and php artisan queue:restart from the panel directory. If Terminal is unavailable, deleting the file bootstrap/cache/config.php in File Manager forces Laravel to rebuild configuration on the next request, achieving the same result without any privileged command.
The queue worker is what actually sends
This is the step most people miss. Pterodactyl queues its mail rather than sending it inline during a web request. If QUEUE_CONNECTION (or the older QUEUE_DRIVER) is set to anything other than sync, the message is written to a jobs table or Redis and sits there until a worker processes it. A panel with no worker running will show "password reset sent" in the UI while the email never leaves the queue. You can confirm this by opening phpMyAdmin, selecting the panel database, and inspecting the jobs table: rows that accumulate and never clear mean the queue is not being drained, while rows in failed_jobs mean the worker ran but the send failed.
On shared hosting you cannot run a persistent daemon or use systemd, so replace the long-running worker with a scheduled cron job that processes whatever is waiting and exits. In cPanel go to Cron Jobs (or DirectAdmin's Cron Jobs) and add an entry that runs every minute. Use the exact PHP binary from your PHP Selector version so the CLI matches the panel's PHP build:
* * * * * /usr/local/bin/php /home/youruser/panel/artisan queue:work --queue=high,standard,low --stop-when-empty >> /dev/null 2>&1
The --stop-when-empty flag is important on shared hosting: the worker drains the queue and then terminates instead of running forever and tripping process-count or CPU limits enforced by CloudLinux. If your account provides a specific PHP path such as /opt/alt/php82/usr/bin/php, use that exact path so the CLI version matches what MultiPHP serves. Pterodactyl also expects the Laravel scheduler to run once a minute for its own housekeeping; that is a separate cron entry running php artisan schedule:run. Keeping both crons distinct avoids one blocking the other. If you prefer the simplest possible setup and email volume is low, set QUEUE_CONNECTION=sync in .env so mail is dispatched during the request itself, though this makes the web request wait on the SMTP handshake and can slow the interface.
DNS records that make mail trusted
Getting SMTP to accept your message is only half the battle; the receiving side still decides whether to trust it. Three DNS records govern that decision, and all are editable from your account's Zone Editor in cPanel or DNS Management in DirectAdmin. First, publish an SPF record as a TXT entry authorizing your server to send for the domain. A typical value is v=spf1 +a +mx include:_spf.yourhost.com ~all; Hostiso auto-creates a workable SPF when you add the domain, so verify it exists rather than duplicating it, because two SPF records invalidate each other.
Second, enable DKIM. In cPanel the Email Deliverability page will flag missing DKIM and offer a one-click Repair that installs the signing key and publishes the matching TXT record automatically. DKIM signs outbound mail so receivers can confirm it was not tampered with in transit, and it is one of the strongest signals against a spam classification. Third, add a DMARC TXT record at _dmarc.yourdomain.com such as v=DMARC1; p=none; rua=mailto:dmarc@yourdomain.com, which tells receivers how to handle failures and gives you reporting while you monitor. Keep the policy at p=none until SPF and DKIM both pass consistently, then tighten to quarantine.
Once records propagate, test end to end. Trigger a password reset from the panel login page, then watch storage/logs/laravel.log in the panel directory through File Manager. Connection refusals, TLS handshake errors, and authentication failures all surface there with a clear stack trace. A Connection could not be established error points to a wrong host, port, or blocked outbound connection; a 535 authentication failed means the mailbox credentials in .env are wrong or the mailbox does not exist. If the log shows the message was handed off successfully but it still lands in spam, the issue is deliverability rather than transport, and the DNS records above are where you focus. The same TLS and outbound-connection concerns apply to any Laravel app making authenticated external calls, a topic covered in our guide on handling outbound webhooks and cURL/TLS on shared hosting. Working through transport, queue, and DNS in that order turns a silent failure into a traceable, fixable path.