<?php
/**
 * @class socketMail
 * @brief Send email through socket
 *
 * @package Clearbricks
 * @subpackage Mail
 *
 * @copyright Olivier Meunier & Association Dotclear
 * @copyright GPL-2.0-only
 */
class socketMail
{
    public static $fp;
    public static $timeout    = 10;   ///< integer: Socket timeout
    public static $smtp_relay = null; ///< string: SMTP Relay to user

    /**
     * Send email through socket
     *
     * This static method sends an email through a simple socket connection.
     * If {@link $smtp_relay} is set, it will be used as a relay to send the
     * email. Instead, email is sent directly to MX host of domain.
     *
     * @param string        $to            Email destination
     * @param string        $subject        Email subject
     * @param string        $message        Email message
     * @param string|array    $headers        Email headers
     * @throws Exception
     */
    public static function mail($to, $subject, $message, $headers = null)
    {
        $from = self::getFrom($headers);

        $H = 'Return-Path: <' . $from . ">\r\n";

        $from_host = explode('@', $from);
        $from_host = $from_host[1];

        $to_host = explode('@', $to);
        $to_host = $to_host[1];

        if (self::$smtp_relay != null) {
            $mx = [gethostbyname(self::$smtp_relay) => 1];
        } else {
            $mx = mail::getMX($to_host);
        }

        foreach ($mx as $h => $w) {
            self::$fp = @fsockopen($h, 25, $errno, $errstr, self::$timeout);

            if (self::$fp !== false) {
                break;
            }
        }

        if (!is_resource(self::$fp)) {
            self::$fp = null;

            throw new Exception('Unable to open socket');
        }

        # We need to read the first line
        fgets(self::$fp);

        $data = '';
        # HELO cmd
        if (!self::cmd('HELO ' . $from_host, $data)) {
            self::quit();

            throw new Exception($data);
        }

        # MAIL FROM: <...>
        if (!self::cmd('MAIL FROM: <' . $from . '>', $data)) {
            self::quit();

            throw new Exception($data);
        }

        # RCPT TO: <...>
        if (!self::cmd('RCPT TO: <' . $to . '>', $data)) {
            self::quit();

            throw new Exception($data);
        }

        # Compose mail and send it with DATA
        $H = 'Return-Path: <' . $from . ">\r\n";
        $H .= 'To: <' . $to . ">\r\n";
        $H .= 'Subject: ' . $subject . "\r\n";
        $H .= $headers . "\r\n";

        $message = $H . "\r\n\r\n" . $message;

        if (!self::sendMessage($message, $data)) {
            self::quit();

            throw new Exception($data);
        }

        self::quit();
    }

    private static function getFrom($headers)
    {
        $f = '';

        if (preg_match('/^from: (.+?)$/msi', $headers, $m)) {
            $f = trim((string) $m[1]);
        }

        if (preg_match('/(?:<)(.+?)(?:$|>)/si', $f, $m)) {
            $f = trim((string) $m[1]);
        } elseif (preg_match('/^(.+?)\(/si', $f, $m)) {
            $f = trim((string) $m[1]);
        } elseif (!text::isEmail($f)) {
            $f = trim((string) ini_get('sendmail_from'));
        }

        if (!$f) {
            throw new Exception('No valid from e-mail address');
        }

        return $f;
    }

    private static function cmd($out, &$data = '')
    {
        fwrite(self::$fp, $out . "\r\n");
        $data = self::data();

        if (substr($data, 0, 3) != '250') {
            return false;
        }

        return true;
    }

    private static function data()
    {
        $s = '';
        stream_set_timeout(self::$fp, 2);

        for ($i = 0; $i < 2; $i++) {
            $s .= fgets(self::$fp, 1024);
        }

        return $s;
    }

    private static function sendMessage($msg, &$data)
    {
        $msg .= "\r\n.";

        self::cmd('DATA', $data);

        if (substr($data, 0, 3) != '354') {
            return false;
        }

        return self::cmd($msg, $data);
    }

    private static function quit()
    {
        self::cmd('QUIT');
        fclose(self::$fp);
        self::$fp = null;
    }
}
