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"