From 2eed78b83e5f3c9f39222f8b588278654da9f5a0 Mon Sep 17 00:00:00 2001 From: Klaus-Uwe Mitterer Date: Fri, 4 Sep 2020 22:01:34 +0200 Subject: [PATCH] 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 --- File.class.php | 7 +- Mail.class.php | 95 ++++++++++++++++++++++++++ Request.class.php | 165 ++++++++++++++++++++++++++++++++++++++++++++++ doc/swagger.yaml | 22 ++++++- sender.php | 83 ++--------------------- 5 files changed, 289 insertions(+), 83 deletions(-) create mode 100644 Mail.class.php create mode 100644 Request.class.php diff --git a/File.class.php b/File.class.php index de88b00..fdb9d7e 100644 --- a/File.class.php +++ b/File.class.php @@ -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) { diff --git a/Mail.class.php b/Mail.class.php new file mode 100644 index 0000000..2613edc --- /dev/null +++ b/Mail.class.php @@ -0,0 +1,95 @@ +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); + } +} \ No newline at end of file diff --git a/Request.class.php b/Request.class.php new file mode 100644 index 0000000..2aa78c8 --- /dev/null +++ b/Request.class.php @@ -0,0 +1,165 @@ +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)); + } + +} \ No newline at end of file diff --git a/doc/swagger.yaml b/doc/swagger.yaml index a005175..5cdcc5e 100644 --- a/doc/swagger.yaml +++ b/doc/swagger.yaml @@ -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 diff --git a/sender.php b/sender.php index 5bb9294..0b221bb 100644 --- a/sender.php +++ b/sender.php @@ -1,86 +1,13 @@ 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"