inclure une version simplifiée de nulib/mail

This commit is contained in:
Jephté Clain 2025-09-10 22:00:43 +04:00
parent ee33699608
commit 6b7d4b683d
11 changed files with 1791 additions and 44 deletions

17
.idea/php.xml generated
View File

@ -55,6 +55,23 @@
<path value="$PROJECT_DIR$/php/vendor/myclabs/deep-copy" />
<path value="$PROJECT_DIR$/php/vendor/nikic/php-parser" />
<path value="$PROJECT_DIR$/php/vendor/composer" />
<path value="$PROJECT_DIR$/php/vendor/league/config" />
<path value="$PROJECT_DIR$/php/vendor/nette/schema" />
<path value="$PROJECT_DIR$/php/vendor/league/commonmark" />
<path value="$PROJECT_DIR$/php/vendor/dflydev/dot-access-data" />
<path value="$PROJECT_DIR$/php/vendor/psr/event-dispatcher" />
<path value="$PROJECT_DIR$/php/vendor/nette/utils" />
<path value="$PROJECT_DIR$/php/vendor/psr/container" />
<path value="$PROJECT_DIR$/php/vendor/psr/log" />
<path value="$PROJECT_DIR$/php/vendor/psr/cache" />
<path value="$PROJECT_DIR$/php/vendor/phpmailer/phpmailer" />
<path value="$PROJECT_DIR$/php/vendor/symfony/expression-language" />
<path value="$PROJECT_DIR$/php/vendor/symfony/cache" />
<path value="$PROJECT_DIR$/php/vendor/symfony/cache-contracts" />
<path value="$PROJECT_DIR$/php/vendor/symfony/polyfill-php80" />
<path value="$PROJECT_DIR$/php/vendor/symfony/service-contracts" />
<path value="$PROJECT_DIR$/php/vendor/symfony/polyfill-php73" />
<path value="$PROJECT_DIR$/php/vendor/symfony/var-exporter" />
</include_path>
</component>
<component name="PhpProjectSharedConfiguration" php_language_level="7.4">

View File

@ -19,6 +19,9 @@
},
"require": {
"symfony/yaml": "^5.0",
"phpmailer/phpmailer": "^6.8",
"symfony/expression-language": "^5.4",
"league/commonmark": "^2.4",
"ext-json": "*",
"php": "^7.4"
},

1377
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,120 @@
<?php
namespace nulib\mail;
use nulib\cv;
use nulib\str;
use nulib\ValueException;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
class MailTemplate {
const SCHEMA = [
"subject" => "string",
"body" => "string",
"exprs" => "array",
];
function __construct(array $mail) {
$tsubject = $mail["subject"] ?? null;
$tbody = $mail["body"] ?? null;
$texprs = $mail["exprs"] ?? [];
$this->el = new ExpressionLanguage();
ValueException::check_null($this->subject = $tsubject, "subject");
ValueException::check_null($this->body = $tbody, "body");
$exprs = [];
# Commencer par extraire les expressions de la forme {name}
if (preg_match_all('/\{([a-zA-Z_][a-zA-Z0-9_.-]*)}/', $this->body, $mss, PREG_SET_ORDER)) {
foreach ($mss as $ms) {
$key = $ms[0];
$expr = str_replace("'", "\\'", $ms[1]);
$expr = "_helper.value('$expr')";
$exprs[$key] = $expr;
}
}
$index = 0;
foreach ($texprs as $key => $expr) {
$prefix = null;
$orig = $expr;
if (preg_match('/^\[([^]]*)]/', $expr, $ms)) {
# un préfixe spécifié de la forme [prefix]expr permet de reconnaitre les
# formes spéciales de expr (+, *, .) qui sont précédées de prefix
# exemple: [https://]+app.url permettra d'utiliser un texte markdown
# de la forme <https://+app.url> qui est correctement reconnu comme un
# url
$prefix = $ms[1];
$expr = substr($expr, strlen($ms[0]));
}
$mapKey = false;
if (str::del_prefix($expr, "+")) {
# config
$mapKey = "$prefix+$expr";
$expr = str_replace("'", "\\'", $expr);
$expr = "_helper.config('$expr')";
} elseif (str::del_prefix($expr, "*")) {
# session
$mapKey = "$prefix*$expr";
$expr = str_replace("'", "\\'", $expr);
$expr = "_helper.session('$expr')";
} elseif (str::del_prefix($expr, ".")) {
# session
$mapKey = "$prefix.$expr";
$expr = str_replace("'", "\\'", $expr);
$expr = "_helper.value('$expr')";
} elseif ($prefix !== null) {
# sinon remettre le préfixe
$expr = $orig;
}
if ($key === $index) {
$index++;
if ($mapKey !== false) {
$exprs[$mapKey] = $expr;
} else {
# clé normale: la correspondance est en minuscule
$exprs[$expr] = strtolower($expr);
}
} else {
$exprs[$key] = $expr;
}
}
uksort($exprs, function ($a, $b) {
return -cv::complen($a, $b);
});
$this->exprs = $exprs;
}
/** @var ExpressionLanguage */
protected $el;
protected $subject;
protected $body;
protected $exprs;
protected function _eval(string $template, ?array $data): string {
if ($data === null) return $template;
$el = $this->el;
foreach ($this->exprs as $key => $expr) {
$value = $el->evaluate($expr, $data);
if (is_array($value)) $value = str::join(" ", $value);
elseif (!is_string($value)) $value = strval($value);
$template = str_replace($key, $value, $template);
}
return $template;
}
function eval(?array $data, $convertMd=true): array {
if ($data !== null) {
$data["_helper"] = new MailTemplateHelper($data);
}
$subject = $this->_eval($this->subject, $data);
$body = $this->body;
if ($convertMd) $body = mdc::convert($body);
$body = $this->_eval($body, $data);
return [
"subject" => $subject,
"body" => $body,
];
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace nulib\mail;
use nulib\cl;
class MailTemplateHelper {
function __construct(?array $data) {
$this->data = $data;
}
function value(string $pkey) {
return cl::pget($this->data, $pkey);
}
function config(string $pkey) {
return config::get($pkey);
}
function session(string $pkey) {
return session::pget($pkey);
}
}

View File

@ -0,0 +1,7 @@
<?php
namespace nulib\mail;
use nulib\UserException;
class MailerException extends UserException {
}

188
php/src/mail/mailer.php Normal file
View File

@ -0,0 +1,188 @@
<?php
namespace nulib\mail;
use nulib\cl;
use nulib\cv;
use nulib\output\msg;
use nulib\str;
use nulib\ValueException;
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\SMTP;
class mailer {
private static function is_bool(&$value): bool {
if ($value === null) {
return false;
} elseif (is_bool($value)) {
return true;
} elseif (is_int($value)) {
$value = boolval($value);
return true;
} else {
switch (strval($value)) {
case "0":
case "no":
case "off":
case "false":
$value = false;
return true;
case "1":
case "yes":
case "on":
case "true":
$value = true;
return true;
default:
return false;
}
}
}
private static function get_bool($value): bool {
if (self::is_bool($value)) return $value;
else return false;
}
const FROM = "no-reply@univ-reunion.fr";
const SCHEMA = [
"backend" => ["string", "smtp"],
"debug" => ["int", SMTP::DEBUG_OFF],
"host" => ["?string", "smtp.univ.run"],
"port" => ["?int", 25],
"auth" => "?bool",
"username" => "?string",
"password" => "?string",
"secure" => "?string",
];
static function get(?array $params=null, ?bool $exceptions=null): PHPMailer {
$mailer = new PHPMailer($exceptions);
$mailer->setLanguage("fr");
$mailer->CharSet = PHPMailer::CHARSET_UTF8;
# backend
$backend = $params["backend"] ?? null;
$backend ??= cv::vn(getenv("NULIB_MAIL_BACKEND"));
$backend ??= "smtp";
switch ($backend) {
case "smtp":
# host
$host = $params["host"] ?? null;
$host ??= cv::vn(getenv("NULIB_MAIL_HOST"));
# port
$port = $params["port"] ?? null;
$port ??= cv::vn(getenv("NULIB_MAIL_PORT"));
$port ??= 25;
if ($host === null) {
throw new ValueException("mail host is required");
}
msg::debug("new PHPMailer using SMTP to $host:$port");
$mailer->isSMTP();
$mailer->Host = $host;
$mailer->Port = $port;
break;
case "phpmail":
msg::debug("new PHPMailer using PHPmail");
$mailer->isMail();
break;
case "sendmail":
msg::debug("new PHPMailer using sendmail");
$mailer->isSendmail();
break;
default:
throw ValueException::invalid_value($backend, "mailer backend");
}
# debug
$debug = $params["debug"] ?? null;
$debug ??= cv::vn(getenv("NULIB_MAIL_DEBUG"));
$debug ??= SMTP::DEBUG_OFF;
if (is_int($debug)) {
if ($debug < SMTP::DEBUG_OFF) $debug = SMTP::DEBUG_OFF;
elseif ($debug > SMTP::DEBUG_LOWLEVEL) $debug = SMTP::DEBUG_LOWLEVEL;
} elseif (!self::is_bool($debug)) {
throw ValueException::invalid_value($debug, "debug mode");
}
$mailer->SMTPDebug = $debug;
# auth, username, password
$username = $params["username"] ?? null;
$username ??= cv::vn(getenv("NULIB_MAIL_USERNAME"));
$password = $params["password"] ?? null;
$password ??= cv::vn(getenv("NULIB_MAIL_PASSWORD"));
$auth = $params["auth"] ?? null;
$auth ??= cv::vn(getenv("NULIB_MAIL_AUTH"));
$auth ??= $username !== null && $password !== null;
$mailer->SMTPAuth = self::get_bool($auth);
$mailer->Username = $username;
$mailer->Password = $password;
# secure
$secure = $params["secure"] ?? null;
$secure ??= cv::vn(getenv("NULIB_MAIL_SECURE"));
$secure ??= false;
if (self::is_bool($secure)) {
if (!$secure) {
$mailer->SMTPSecure = "";
$mailer->SMTPAutoTLS = false;
}
} else {
switch ($secure) {
case PHPMailer::ENCRYPTION_SMTPS:
case PHPMailer::ENCRYPTION_STARTTLS:
$mailer->SMTPSecure = $secure;
break;
default:
throw ValueException::invalid_value($secure, "encryption mode");
}
}
return $mailer;
}
static function build($to, string $subject, string $body, $cc=null, $bcc=null, ?string $from=null, ?PHPMailer $mailer=null): PHPMailer {
if ($mailer === null) $mailer = self::get();
$mailer->clearAllRecipients();
if ($from === null) $from = static::FROM;
$mailer->setFrom($from);
foreach (cl::with($to) as $tos) {
foreach (preg_split('/\s*[,;]\s*/', trim($tos)) as $to) {
$mailer->addAddress($to);
}
}
foreach (cl::with($cc) as $ccs) {
foreach (preg_split('/\s*[,;]\s*/', trim($ccs)) as $cc) {
$mailer->addCC($cc);
}
}
foreach (cl::with($bcc) as $bccs) {
foreach (preg_split('/\s*[,;]\s*/', trim($bccs)) as $bcc) {
$mailer->addBCC($bcc);
}
}
$mailer->isHTML();
$mailer->Subject = $subject;
$mailer->Body = $body;
return $mailer;
}
static function _send(PHPMailer $mailer): void {
$tos = [];
foreach ($mailer->getToAddresses() as $to) {
$tos[] = $to[0];
}
$tos = str::join(",", $tos);
msg::debug("Sending to $tos");
if (!$mailer->send()) {
throw new MailerException("Une erreur s'est produite pendant l'envoi du mail", $mailer->ErrorInfo);
}
}
static function send($to, string $subject, string $body, $cc=null, $bcc=null, ?string $from=null, ?PHPMailer $mailer=null): void {
self::_send(self::build($to, $subject, $body, $cc, $bcc, $from, $mailer));
}
static function tsend(array $template, array $data, $to, $cc=null, $bcc=null, ?string $from=null): void {
$template = new MailTemplate($template);
$mail = $template->eval($data);
self::send($to, $mail["subject"], $mail["body"], $cc, $bcc, $from);
}
}

22
php/src/mail/mdc.php Normal file
View File

@ -0,0 +1,22 @@
<?php
namespace nulib\mail;
use League\CommonMark\GithubFlavoredMarkdownConverter;
use League\CommonMark\MarkdownConverter;
class mdc {
private static $mdc;
static function mdc(): MarkdownConverter {
if (self::$mdc === null) {
self::$mdc = new GithubFlavoredMarkdownConverter([
"allow_unsafe_links" => false,
]);
}
return self::$mdc;
}
static function convert(string $text): string {
return self::mdc()->convert($text);
}
}

26
php/tbin/mail.php Executable file
View File

@ -0,0 +1,26 @@
#!/usr/bin/php
<?php
require __DIR__.'/../vendor/autoload.php';
use lib\mail\mailer;
use nur\cli\Application;
Application::run(new class extends Application {
const ARGS = [
"merge" => parent::ARGS,
["-t", "--to", "args" => 1, "action" => "--add", "name" => "to"],
["-c", "--cc", "args" => 1, "action" => "--add", "name" => "cc"],
["-b", "--bcc", "args" => 1, "action" => "--add", "name" => "bcc"],
["-F", "--from", "args" => 1, "name" => "from"],
["args" => 2, "name" => "args"],
];
protected $to, $cc, $bcc, $from;
protected $args;
function main() {
$subject = $this->args[0];
$body = $this->args[1];
mailer::send($this->to, $subject, $body, $this->cc, $this->bcc, $this->from);
}
});

19
php/tbin/test_mail.php Normal file
View File

@ -0,0 +1,19 @@
<?php
require __DIR__.'/../vendor/autoload.php';
use nulib\mail\mailer;
putenv("NULIB_MAIL_HOST=maildev.devel.self");
$template = [
"subject" => "test de mail",
"body" => <<<EOF
bonjour,
ceci est un test de mail pour {dest}
EOF
];
$data = [
"dest" => "moi même",
];
mailer::tsend($template, $data, "jephte.clain@gmail.com");

View File

@ -0,0 +1,34 @@
<?php
namespace nulib\mail;
use nur\t\TestCase;
class MailTemplateTest extends TestCase {
function testTemplate() {
$mail = [
"subject" => "infos pour NOM PRENOM",
"body" => <<<EOT
bonjour PRENOM NOM,
vous avez AGE ans
EOT,
"exprs" => [
"PRENOM" => "prenom",
"NOM" => "nom",
"AGE" => "age",
],
];
$tpl = new MailTemplate($mail);
[
"subject" => $subject,
"body" => $body,
] = $tpl->eval([
"nom" => "Clain",
"prenom" => "Jephté",
"age" => 47,
]);
self::assertSame("infos pour Clain Jephté", $subject);
self::assertSame("<p>bonjour Jephté Clain,</p>\n<p>vous avez 47 ans</p>\n", $body);
}
}