Complete refactor: Moving stuff to classes, making sender tiny

Adding two config options to modify behavior
Allow messages with empty body to be sent
Automatically convert HTML to plain if none provided
Version bump to 0.4
This commit is contained in:
Kumi 2020-09-04 22:01:34 +02:00
parent 41a337d875
commit 2eed78b83e
5 changed files with 289 additions and 83 deletions

View file

@ -35,8 +35,11 @@ class File {
return $headers;
}
function get_status($follow=true) {
return $this->get_headers($follow)["status"];
function get_status($follow=true, $throw=true) {
if ($status=$this->get_headers($follow)["status"] >= 400) {
throw new Exception("Error downloading " . $this->url . " - Status: " . $status);
}
return $status;
}
function get_filename($follow=true) {

95
Mail.class.php Normal file
View file

@ -0,0 +1,95 @@
<?php
require_once 'vendor/autoload.php';
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;
class Mail {
private $mailer;
function __construct($host=null, $user=null, $password=null, $port=null) {
global $MAIL_HOST, $MAIL_USER, $MAIL_PASS, $MAIL_STARTTLS, $MAIL_SMTPS, $MAIL_PORT;
$this->mailer = new PHPMailer(true);
$this->mailer->isSMTP();
$this->mailer->AllowEmpty = true;
$this->mailer->Host = $host ?: $MAIL_HOST;
$this->mailer->SMTPAuth = (bool) $MAIL_USER;
$this->mailer->Username = $user ?: $MAIL_USER;
$this->mailer->Password = $password ?: $MAIL_PASS;
$this->mailer->SMTPSecure = $MAIL_STARTTLS ? PHPMailer::ENCRYPTION_STARTTLS : ($MAIL_SMTPS ? PHPMailer::ENCRYPTION_SMTPS : false);
$this->mailer->Port = $port ?: ($MAIL_PORT ?: ($MAIL_SMTPS ? 465 : 587));
$this->mailer->XMailer = "Kumi Systems Mailer 0.1 (https://kumi.systems/)";
}
function add_attachment($content, $filename=null, $cid=null) {
$this->mailer->addStringEmbeddedImage($content, $cid, $filename);
}
function add_attachments($attachments) {
foreach ($attachments as $attachment) {
$this->add_attachment($attachment["content"], $attachment["filename"], $attachment["cid"]);
}
}
function add_bcc($email, $name) {
$this->mailer->addBCC($email, $name);
}
function add_bccs($bccs) {
foreach ($bccs as $bcc) {
$this->add_bcc($bcc["email"], $bcc["name"]);
}
}
function add_cc($email, $name) {
$this->mailer->addCC($email, $name);
}
function add_ccs($ccs) {
foreach ($ccs as $cc) {
$this->add_cc($cc["email"], $cc["name"]);
}
}
function add_content($html=null, $text=null) {
$this->mailer->isHTML((bool) $html);
$this->mailer->Body = $html ?: $text;
$this->mailer->AltBody = $html ? $text : null;
}
function add_recipient($email, $name) {
$this->mailer->addAddress($email, $name);
}
function add_recipients($recipients) {
foreach ($recipients as $recipient) {
$this->add_recipient($recipient["email"], $recipient["name"]);
}
}
static function FromRequest($req) {
$mail = new self();
$mail->set_from($req->get_sender()["email"], $req->get_sender()["name"]);
$mail->add_recipients($req->get_recipients());
$mail->add_ccs($req->get_ccs());
$mail->add_bccs($req->get_bccs());
$html = $req->prepare_html();
$text = $req->prepare_text(!$req->has_config("noconversion"));
$mail->add_content($html, $text);
$mail->add_attachments($req->get_attachments(true, true, $req->has_config("ignoredlfails")));
return $mail;
}
function send() {
$this->mailer->send();
}
function set_from($email, $name) {
$this->mailer->setFrom($email, $name);
}
}

165
Request.class.php Normal file
View file

@ -0,0 +1,165 @@
<?php
require_once("config.php");
require_once("File.class.php");
class Request {
public $json;
function __construct($data, $authorize=True) {
if (!$json=json_decode($data, true)) {
throw new Exception("There is nothing here.");
};
$this->json = array_change_key_case($json, CASE_LOWER);
if ($authorize) {
if (!$this->authorize()) {
throw new Exception("Who are you?");
}
}
}
function authorize() {
global $API_KEYS;
return in_array($this->get_key(), $API_KEYS);
}
function get_attachments($resolve=true, $content=true, $ignoredlfails=false) {
$outachments = array();
foreach ($this->json["attachments"] as $attachment) {
$attachment["content"] = "";
if ($resolve || $content) {
$file = new File($attachment["url"]);
if ($resolve && !$attachment["filename"]) {
$attachment["filename"] = $file->get_filename();
}
if ($content) {
try {
$attachment["content"] = $file->fetch_file();
} catch (\Exception $e) {
if (!$ignoredlfails) {
throw $e;
}
}
}
}
if ($attachment["content"] || !$content) {
array_push($outachments, $attachment);
}
}
return $outachments;
}
function get_bccs($default=true) {
global $ALWAYS_BCC;
$bccs = $this->json["bccs"] ?: array();
if ($default) {
foreach ($ALWAYS_BCC as $bcc) {
array_push($bccs, array("email"=>$bcc));
}
}
return $bccs;
}
function get_ccs($default=true) {
global $ALWAYS_CC;
$ccs = $this->json["ccs"] ?: array();
if ($default) {
foreach ($ALWAYS_CC as $cc) {
array_push($ccs, array("email"=>$cc));
}
}
return $ccs;
}
function get_config() {
return $this->json["config"];
}
function get_html() {
if ($this->json["html"]) {
$html = $this->json["html"];
} else if ($this->json["htmlurl"]) {
try {
$htmlfile = new File($this->json["htmlurl"]);
$html = $htmlfile->fetch_file();
} catch (\Exception $e) {
throw new Exception("Could not fetch URL for HTML message: " . $e->getMessage());
}
}
return $html;
}
function get_key() {
return $this->json["key"];
}
function get_placeholders() {
return $this->json["placeholders"];
}
function get_recipients($default=true) {
global $ALWAYS_TO;
$recipients = $this->json["recipients"] ?: array();
if ($default) {
foreach ($ALWAYS_TO as $recipient) {
array_push($recipients, array("email"=>$recipient));
}
}
return $recipients;
}
function get_sender($fallback=true) {
global $MAIL_FROM_MAIL, $MAIL_FROM_NAME, $MAIL_USER;
if ($this->json["sender"]) {
return $this->json["sender"];
} else if ($fallback) {
return array(
"email" => $MAIL_FROM_MAIL ? $MAIL_FROM_MAIL : $MAIL_USER,
"name" => $MAIL_FROM_NAME
);
}
}
function get_text($conversion=true) {
if ($this->json["text"]) {
$text = $this->json["text"];
} else if ($this->json["texturl"]) {
try {
$textfile = new File($this->json["texturl"]);
$text = $textfile->fetch_file();
} catch (\Exception $e) {
throw new Exception("Could not fetch URL for plain text message: " . $e->getMessage());
}
} else if ($conversion) {
if ($html=$this->get_html()) {
$text = html_entity_decode(
trim(strip_tags(preg_replace('/<(head|title|style|script)[^>]*>.*?<\/\\1>/si', '', $html))),
ENT_QUOTES
);
}
}
return $text;
}
function has_config($key) {
return in_array($key, $this->get_config());
}
function prepare_html() {
return $this->prepare_string($this->get_html());
}
function prepare_string($string) {
foreach ($this->get_placeholders() as $placeholder) $string = str_replace("{".strtoupper($placeholder["name"])."}", $placeholder["value"], $string);
return $string;
}
function prepare_text($conversion=true) {
return $this->prepare_string($this->get_text($conversion));
}
}

View file

@ -1,7 +1,7 @@
openapi: 3.0.0
info:
description: A simple endpoint to send email messages
version: '0.3'
version: '0.4'
title: EXPMail
contact:
email: support@kumi.systems
@ -40,6 +40,17 @@ servers:
- url: 'https://expmail.kumi.live'
components:
schemas:
Config:
type: object
required:
- key
properties:
key:
type: string
description: >
Key of the config setting
* `noconversion` - Do not automatically convert HTML to plain text if no plain text message is explicitly given
* `ignoredlfails` - If an attachment fails to download, just leave it out and continue processing the message
Placeholder:
type: object
required:
@ -89,10 +100,10 @@ components:
description: Subject of the email
html:
type: string
description: 'String containing the HTML content of the email. Takes precedence over `htmlurl` if provided. If both `html` and `text` or `texturl` are provided, will create a multi-part MIME message.'
description: 'String containing the HTML content of the email. Takes precedence over `htmlurl` if provided. If both `html` and `text` or `texturl` are provided, or the `noconversion` config key is not provided, will create a multi-part MIME message.'
htmlurl:
type: string
description: 'String containing the URL to a file containing the HTML content of the email. Ignored if `html` if provided. If both `htmlurl` and `text` or `texturl` are provided, will create a multi-part MIME message.'
description: 'String containing the URL to a file containing the HTML content of the email. Ignored if `html` if provided. If both `htmlurl` and `text` or `texturl` are provided, or the `noconversion` config key is not provided, will create a multi-part MIME message.'
text:
type: string
description: 'String containing the plain text content of the email. Takes precedence over `texturl` if provided. If both `text` and `html` or `htmlurl` are provided, will create a multi-part MIME message.'
@ -128,6 +139,11 @@ components:
description: 'Array of `Placeholder` objects. Any occurrences of `{PLACEHOLDER_NAME}` (`name` in all caps enclosed with curly brackets) in the email''s HTML or plain text will be replaced by `value`.'
items:
$ref: '#/components/schemas/Placeholder'
config:
type: array
description: 'Array of `Config` objects to change the endpoint\'s default behaviour'
items:
$ref: '#/components/schemas/Config'
key:
type: string
description: API key to authenticate request with

View file

@ -1,86 +1,13 @@
<?php
require_once 'vendor/autoload.php';
require_once 'config.php';
require_once 'File.class.php';
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;
$mailer = new PHPMailer(true);
require_once 'Mail.class.php';
require_once 'Request.class.php';
try {
if (!$json = json_decode(file_get_contents('php://input'), true)) throw new Exception("There is nothing here.");
if (!$json["key"]) throw new Exception("What's the code word?");
if (!in_array($json["key"], $API_KEYS)) throw new Exception("Who are you?");
if ($json["html"]) {
$html = $json["html"];
} else if ($json["htmlurl"]) {
try {
$htmlfile = new File($json["htmlurl"]);
$html = $htmlfile->fetch_file();
} catch (\Exception $e) {
throw new Exception("Could not fetch URL for HTML message: " . $e->getMessage());
}
}
if ($json["text"]) {
$text = $json["text"];
} else if ($json["texturl"]) {
try {
$textfile = new File($json["texturl"]);
$text = $textfile->fetch_file();
} catch (\Exception $e) {
throw new Exception("Could not fetch URL for plain text message: " . $e->getMessage());
}
}
foreach ($json["placeholders"] as $placeholder) {
$html = str_replace("{".strtoupper($placeholder["name"])."}", $placeholder["value"], $html);
$text = str_replace("{".strtoupper($placeholder["name"])."}", $placeholder["value"], $text);
}
$mailer->isSMTP();
$mailer->Host = $MAIL_HOST;
$mailer->SMTPAuth = (bool) $MAIL_USER;
$mailer->Username = $MAIL_USER;
$mailer->Password = $MAIL_PASS;
$mailer->SMTPSecure = ($MAIL_STARTTLS ? PHPMailer::ENCRYPTION_STARTTLS : ($MAIL_SMTPS ? PHPMailer::ENCRYPTION_SMTPS : false));
$mailer->Port = ($MAIL_PORT ? $MAIL_PORT : ($MAIL_SMTPS ? 465 : 587));
$mailer->XMailer = "Kumi Systems Mailer 0.1 (https://kumi.systems/)";
if ($json["sender"]["email"]) {
$mailer->setFrom($json["sender"]["email"], $json["sender"]["name"]);
} else {
$mailer->setFrom(($MAIL_FROM_MAIL ? $MAIL_FROM_MAIL : $MAIL_USER), $MAIL_FROM_NAME);
};
foreach ($json["recipients"] as $recipient) $mailer->addAddress($recipient["email"], $recipient["name"]);
foreach ($json["ccs"] as $cc) $mailer->addCC($cc["email"], $cc["name"]);
foreach ($json["bccs"] as $bcc) $mailer->addBCC($bcc["email"], $bcc["name"]);
foreach ($ALWAYS_TO as $recipient) $mailer->addAddress($recipient);
foreach ($ALWAYS_CC as $cc) $mailer->addCC($cc);
foreach ($ALWAYS_BCC as $bcc) $mailer->addBCC($bcc);
$mailer->isHTML((bool) $html);
$mailer->Subject = $json["subject"];
$mailer->Body = ($html ? $html : $text);
$mailer->AltBody = ($html ? $text : null);
foreach ($json["attachments"] as $attachment) {
$file = new File($attachment["url"]);
if ($file->get_status() >= 400) throw new Exception("Error downloading " . $attachment["url"] . " - Status: " . $file->get_status());
$content = $file->fetch_file();
$filename = ($attachment["filename"] ? $attachment["filename"] : $file->get_filename());
$cid = ($attachment["cid"] ? $attachment["cid"] : 0);
$mailer->addStringEmbeddedImage($content, $cid, $filename);
}
$mailer->send();
$req = new Request(file_get_contents('php://input'));
$mail = Mail::FromRequest($req);
$mail->send();
$response = array(
"status" => "success"