diff --git a/WpMailCatcher.php b/WpMailCatcher.php index 874f15e..881f2e7 100644 --- a/WpMailCatcher.php +++ b/WpMailCatcher.php @@ -6,7 +6,7 @@ Domain Path: /languages Description: Logging your mail will stop you from ever losing your emails again! This fast, lightweight plugin (under 140kb in size!) is also useful for debugging or backing up your messages. Author: James Ward -Version: 2.1.3 +Version: 2.1.4 Author URI: https://jamesward.io Donate link: https://paypal.me/jamesmward */ diff --git a/build/grunt/package-lock.json b/build/grunt/package-lock.json index 249d31f..45c2839 100644 --- a/build/grunt/package-lock.json +++ b/build/grunt/package-lock.json @@ -1,6 +1,6 @@ { "name": "WpMailCatcher", - "version": "2.1.1", + "version": "2.1.4", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/build/grunt/package.json b/build/grunt/package.json index 85cb2a8..2b96d17 100644 --- a/build/grunt/package.json +++ b/build/grunt/package.json @@ -1,6 +1,6 @@ { "name": "WpMailCatcher", - "version": "2.1.3", + "version": "2.1.4", "lang_po_directory": "../../languages", "build_directory": "./..", "dist_directory": "../../assets", diff --git a/composer.json b/composer.json index 6c3fb2e..95c5b80 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,8 @@ "szepeviktor/phpstan-wordpress": "^1.1", "phpstan/extension-installer": "^1.1", "phpstan/phpstan-phpunit": "^1.3", - "squizlabs/php_codesniffer": "3.*" + "squizlabs/php_codesniffer": "3.*", + "mockery/mockery": "^1.6" }, "config": { "allow-plugins": { diff --git a/composer.lock b/composer.lock index a06b274..47c6178 100644 --- a/composer.lock +++ b/composer.lock @@ -4,9 +4,102 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "83ba92eacc0fad97d37bb9a72e38cc82", + "content-hash": "01fa34b9135e01ed7e25ef51a3aac754", "packages": [], "packages-dev": [ + { + "name": "10up/wp_mock", + "version": "0.4.2", + "source": { + "type": "git", + "url": "https://github.com/10up/wp_mock.git", + "reference": "9019226eb50df275aa86bb15bfc98a619601ee49" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/10up/wp_mock/zipball/9019226eb50df275aa86bb15bfc98a619601ee49", + "reference": "9019226eb50df275aa86bb15bfc98a619601ee49", + "shasum": "" + }, + "require": { + "antecedent/patchwork": "^2.1", + "mockery/mockery": "^1.0", + "php": ">=7.1", + "phpunit/phpunit": ">=7.0" + }, + "require-dev": { + "behat/behat": "^3.0", + "php-coveralls/php-coveralls": "^2.1", + "sebastian/comparator": ">=1.2.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "WP_Mock\\": "./php/WP_Mock" + }, + "classmap": [ + "php/WP_Mock.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "description": "A mocking library to take the pain out of unit testing for WordPress", + "support": { + "issues": "https://github.com/10up/wp_mock/issues", + "source": "https://github.com/10up/wp_mock/tree/master" + }, + "time": "2019-03-16T03:44:39+00:00" + }, + { + "name": "antecedent/patchwork", + "version": "2.1.26", + "source": { + "type": "git", + "url": "https://github.com/antecedent/patchwork.git", + "reference": "f2dae0851b2eae4c51969af740fdd0356d7f8f55" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/antecedent/patchwork/zipball/f2dae0851b2eae4c51969af740fdd0356d7f8f55", + "reference": "f2dae0851b2eae4c51969af740fdd0356d7f8f55", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": ">=4" + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignas Rudaitis", + "email": "ignas.rudaitis@gmail.com" + } + ], + "description": "Method redefinition (monkey-patching) functionality for PHP.", + "homepage": "http://patchwork2.org/", + "keywords": [ + "aop", + "aspect", + "interception", + "monkeypatching", + "redefinition", + "runkit", + "testing" + ], + "support": { + "issues": "https://github.com/antecedent/patchwork/issues", + "source": "https://github.com/antecedent/patchwork/tree/2.1.26" + }, + "time": "2023-09-18T08:18:37+00:00" + }, { "name": "doctrine/instantiator", "version": "1.5.0", @@ -77,6 +170,142 @@ ], "time": "2022-12-30T00:15:36+00:00" }, + { + "name": "hamcrest/hamcrest-php", + "version": "v2.0.1", + "source": { + "type": "git", + "url": "https://github.com/hamcrest/hamcrest-php.git", + "reference": "8c3d0a3f6af734494ad8f6fbbee0ba92422859f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/8c3d0a3f6af734494ad8f6fbbee0ba92422859f3", + "reference": "8c3d0a3f6af734494ad8f6fbbee0ba92422859f3", + "shasum": "" + }, + "require": { + "php": "^5.3|^7.0|^8.0" + }, + "replace": { + "cordoval/hamcrest-php": "*", + "davedevelopment/hamcrest-php": "*", + "kodova/hamcrest-php": "*" + }, + "require-dev": { + "phpunit/php-file-iterator": "^1.4 || ^2.0", + "phpunit/phpunit": "^4.8.36 || ^5.7 || ^6.5 || ^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1-dev" + } + }, + "autoload": { + "classmap": [ + "hamcrest" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "This is the PHP port of Hamcrest Matchers", + "keywords": [ + "test" + ], + "support": { + "issues": "https://github.com/hamcrest/hamcrest-php/issues", + "source": "https://github.com/hamcrest/hamcrest-php/tree/v2.0.1" + }, + "time": "2020-07-09T08:09:16+00:00" + }, + { + "name": "mockery/mockery", + "version": "1.6.6", + "source": { + "type": "git", + "url": "https://github.com/mockery/mockery.git", + "reference": "b8e0bb7d8c604046539c1115994632c74dcb361e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mockery/mockery/zipball/b8e0bb7d8c604046539c1115994632c74dcb361e", + "reference": "b8e0bb7d8c604046539c1115994632c74dcb361e", + "shasum": "" + }, + "require": { + "hamcrest/hamcrest-php": "^2.0.1", + "lib-pcre": ">=7.0", + "php": ">=7.3" + }, + "conflict": { + "phpunit/phpunit": "<8.0" + }, + "require-dev": { + "phpunit/phpunit": "^8.5 || ^9.6.10", + "psalm/plugin-phpunit": "^0.18.4", + "symplify/easy-coding-standard": "^11.5.0", + "vimeo/psalm": "^4.30" + }, + "type": "library", + "autoload": { + "files": [ + "library/helpers.php", + "library/Mockery.php" + ], + "psr-4": { + "Mockery\\": "library/Mockery" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Pádraic Brady", + "email": "padraic.brady@gmail.com", + "homepage": "https://github.com/padraic", + "role": "Author" + }, + { + "name": "Dave Marshall", + "email": "dave.marshall@atstsolutions.co.uk", + "homepage": "https://davedevelopment.co.uk", + "role": "Developer" + }, + { + "name": "Nathanael Esayeas", + "email": "nathanael.esayeas@protonmail.com", + "homepage": "https://github.com/ghostwriter", + "role": "Lead Developer" + } + ], + "description": "Mockery is a simple yet flexible PHP mock object framework", + "homepage": "https://github.com/mockery/mockery", + "keywords": [ + "BDD", + "TDD", + "library", + "mock", + "mock objects", + "mockery", + "stub", + "test", + "test double", + "testing" + ], + "support": { + "docs": "https://docs.mockery.io/", + "issues": "https://github.com/mockery/mockery/issues", + "rss": "https://github.com/mockery/mockery/releases.atom", + "security": "https://github.com/mockery/mockery/security/advisories", + "source": "https://github.com/mockery/mockery" + }, + "time": "2023-08-09T00:03:52+00:00" + }, { "name": "myclabs/deep-copy", "version": "1.11.1", @@ -1894,10 +2123,10 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": ">=7.2", + "php": ">=7.4", "ext-json": "*", "ext-mbstring": "*" }, "platform-dev": [], - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } diff --git a/entrypoint.sh b/entrypoint.sh index f274407..e1b8a4d 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -4,7 +4,7 @@ export DB_DATABASE=wordpress export DB_USERNAME=wp_mail_catcher export DB_PASSWORD=password export PHP_VERSION=8.0 -export WP_VERSION=6.2 +export WP_VERSION=latest CMD=$1 diff --git a/languages/WpMailCatcher.pot b/languages/WpMailCatcher.pot index ae4a04d..a7a4fcb 100644 --- a/languages/WpMailCatcher.pot +++ b/languages/WpMailCatcher.pot @@ -6,9 +6,9 @@ #, fuzzy msgid "" msgstr "" -"Project-Id-Version: WpMailCatcher 2.1.0\n" +"Project-Id-Version: WpMailCatcher 2.1.4\n" "Report-Msgid-Bugs-To: wordpress@jamesward.io\n" -"POT-Creation-Date: 2023-05-31 22:22+0000\n" +"POT-Creation-Date: 2023-10-28 12:40+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -30,55 +30,55 @@ msgstr "" msgid "Once a month" msgstr "" -#: ../../src/GeneralHelper.php:218 +#: ../../src/GeneralHelper.php:221 #, php-format msgctxt "%s = human-readable time difference" msgid "%s" msgstr "" -#: ../../src/MailAdminTable.php:61 +#: ../../src/MailAdminTable.php:75 msgid "This subject was base64 decoded" msgstr "" -#: ../../src/MailAdminTable.php:76 +#: ../../src/MailAdminTable.php:92 msgid "This subject was quoted printable decoded" msgstr "" -#: ../../src/MailAdminTable.php:101 +#: ../../src/MailAdminTable.php:117 msgid "More Info" msgstr "" -#: ../../src/MailAdminTable.php:109 ../../src/Views/NewMessageModal.php:30 +#: ../../src/MailAdminTable.php:125 ../../src/Views/NewMessageModal.php:30 msgid "To" msgstr "" -#: ../../src/MailAdminTable.php:110 ../../src/Views/NewMessageModal.php:53 +#: ../../src/MailAdminTable.php:126 ../../src/Views/NewMessageModal.php:53 msgid "Subject" msgstr "" -#: ../../src/MailAdminTable.php:111 ../../src/Views/NewMessageModal.php:33 +#: ../../src/MailAdminTable.php:127 ../../src/Views/NewMessageModal.php:33 msgid "From" msgstr "" -#: ../../src/MailAdminTable.php:112 +#: ../../src/MailAdminTable.php:128 msgid "Sent" msgstr "" -#: ../../src/MailAdminTable.php:120 ../../src/MailAdminTable.php:159 +#: ../../src/MailAdminTable.php:136 ../../src/MailAdminTable.php:177 msgid "Delete" msgstr "" -#: ../../src/MailAdminTable.php:121 ../../src/MailAdminTable.php:160 +#: ../../src/MailAdminTable.php:137 ../../src/MailAdminTable.php:178 #: ../../src/Views/LogModal.php:92 msgid "Resend" msgstr "" -#: ../../src/MailAdminTable.php:122 ../../src/MailAdminTable.php:161 +#: ../../src/MailAdminTable.php:138 ../../src/MailAdminTable.php:179 #: ../../src/Views/ExportWarningDialog.php:81 msgid "Export" msgstr "" -#: ../../src/MailAdminTable.php:123 +#: ../../src/MailAdminTable.php:139 msgid "View" msgstr "" diff --git a/readme.txt b/readme.txt index faa219a..a256634 100644 --- a/readme.txt +++ b/readme.txt @@ -2,9 +2,9 @@ Contributors: Wardee Tags: mail logging, email log, email logger, logging, email logging, mail, crm Requires at least: 4.7 -Tested up to: 6.2.3 +Tested up to: 6.4 Requires PHP: 7.4 -Stable tag: 2.1.3 +Stable tag: 2.1.4 License: GNU General Public License v3.0 License URI: https://raw.githubusercontent.com/JWardee/wp-mail-catcher/master/LICENSE Donate link: https://paypal.me/jamesmward @@ -94,6 +94,10 @@ Great! Please leave a note in our (GitHub tracker) == Changelog == += 2.1.4 = + +- Security: Fixed Injection vulnerability, reported by Muhammad Daffa via Patchstack + = 2.1.3 = - Fix: Improved HTML email detection diff --git a/src/GeneralHelper.php b/src/GeneralHelper.php index eb29b34..04d23ed 100644 --- a/src/GeneralHelper.php +++ b/src/GeneralHelper.php @@ -5,7 +5,7 @@ class GeneralHelper { public static $csvItemDelimiter = ' | '; - public static $logsPerPage = 5; + public static $logsPerPage = 20; public static $pluginPath; public static $pluginUrl; public static $pluginVersion; @@ -132,22 +132,6 @@ public static function labelToSlug($label) return strtolower($label); } - public static function sanitiseForDbQuery($value) - { - switch (gettype($value)) { - case ('array'): - array_walk_recursive($value, function (&$value) { - $value = sanitize_text_field($value); - }); - break; - default: - $value = sanitize_text_field($value); - break; - } - - return $value; - } - private static function getAllowedTags() { $tags = wp_kses_allowed_html('post'); @@ -168,21 +152,28 @@ public static function getAttachmentIdsFromUrl($urls) global $wpdb; - $urls = self::sanitiseForDbQuery($urls); - $sql = "SELECT DISTINCT post_id FROM " . $wpdb->prefix . "postmeta - WHERE meta_value LIKE '%" . $urls[0] . "%'"; + WHERE meta_value LIKE %s"; if (is_array($urls) && count($urls) > 1) { - array_shift($urls); foreach ($urls as $url) { - $sql .= " OR meta_value LIKE '%" . $url . "%'"; + // Skip first url as it's covered above + if ($url === $urls[0]) { + continue; + } + + $sql .= " OR meta_value LIKE %s"; } } $sql .= " AND meta_key = '_wp_attached_file'"; + $urls = array_map(function ($url) { + return '%' . $url . '%'; + }, $urls); + + $sql = $wpdb->prepare($sql, $urls); $results = $wpdb->get_results($sql, ARRAY_N); if (isset($results[0])) { @@ -252,11 +243,4 @@ public static function getPrefixedSlug($slugOrLabel) { return self::$namespacePrefix . self::labelToSlug($slugOrLabel); } - - public static function dd($value) - { - echo '
';
-        print_r($value);
-        exit;
-    }
 }
diff --git a/src/MailAdminTable.php b/src/MailAdminTable.php
index 3651ac6..723bec4 100644
--- a/src/MailAdminTable.php
+++ b/src/MailAdminTable.php
@@ -31,7 +31,7 @@ public static function getInstance()
         return self::$instance;
     }
 
-    private function runHtmlSpecialChars($value)
+    function runHtmlSpecialChars($value)
     {
         $value = GeneralHelper::filterHtml($value);
 
diff --git a/src/Models/Logs.php b/src/Models/Logs.php
index 7eddf65..95c10c4 100644
--- a/src/Models/Logs.php
+++ b/src/Models/Logs.php
@@ -35,7 +35,7 @@ public static function getFirst($args = [])
     /**
      * @param  array  $args
      *
-     * @return array|null|object
+     * @return array|null|object|string
      */
     public static function get(array $args = [])
     {
@@ -75,25 +75,27 @@ public static function get(array $args = [])
             'additional_headers'
         ];
 
+        if (!is_array($args['post__in'])) {
+            $args['post__in'] = [$args['post__in']];
+        }
+
         if (Settings::get('db_version') >= '2.0.0') {
             $defaultColumns[] = 'is_html';
         }
 
         $columnsToSelect = array_diff($defaultColumns, $args['column_blacklist']);
-
-        /**
-         * Sanitise each value in the array
-         */
-        array_walk_recursive($args, 'WpMailCatcher\GeneralHelper::sanitiseForDbQuery');
-
-        $sql = "SELECT " . implode(',', $columnsToSelect) . "
-            FROM " . $wpdb->prefix . GeneralHelper::$tableName . " ";
+        $placeholderValues = $columnsToSelect;
+        $columnToSelectPlaceholders = array_fill(0, count($columnsToSelect), '%i');
+        $sql = "SELECT " . implode(',', $columnToSelectPlaceholders) . "
+                FROM " . $wpdb->prefix . GeneralHelper::$tableName . " ";
 
         $whereClause = false;
 
         if (!empty($args['post__in'])) {
             $whereClause = true;
-            $sql .= "WHERE id IN(" . GeneralHelper::arrayToString($args['post__in']) . ") ";
+            $postInPlaceholders = array_fill(0, count($args['post__in']), '%s');
+            $sql .= "WHERE id IN(" . implode(',', $postInPlaceholders) . ") ";
+            $placeholderValues = array_merge($placeholderValues, $args['post__in']);
         }
 
         if ($args['subject'] != null && $args['s'] == null) {
@@ -108,11 +110,16 @@ public static function get(array $args = [])
                 $whereClause = true;
             }
 
-            $sql .= "(subject LIKE '%" . $args['s'] . "%') OR ";
-            $sql .= "(message LIKE '%" . $args['s'] . "%') OR ";
-            $sql .= "(email_to LIKE '%" . $args['s'] . "%') OR ";
-            $sql .= "(attachments LIKE '%" . $args['s'] . "%') OR ";
-            $sql .= "(additional_headers LIKE '%" . $args['s'] . "%') ";
+            $sql .= "(subject LIKE %s) OR ";
+            $sql .= "(message LIKE %s) OR ";
+            $sql .= "(email_to LIKE %s) OR ";
+            $sql .= "(attachments LIKE %s) OR ";
+            $sql .= "(additional_headers LIKE %s) ";
+
+            $placeholderValues = array_merge(
+                $placeholderValues,
+                array_fill(0, 5, '%' . $args['s'] . '%')
+            );
         }
 
         if ($args['post_status'] != 'any') {
@@ -132,16 +139,26 @@ public static function get(array $args = [])
             }
         }
 
-        $sql .= "ORDER BY " . $args['orderby'] . " " . $args['order'] . " ";
+        $order = strtolower($args['order']) === "desc" ? "DESC" : "ASC";
+        $sql .= "ORDER BY %i " . $order . " ";
+        $placeholderValues = array_merge($placeholderValues, [
+            $args['orderby']
+        ]);
 
         if ($args['posts_per_page'] != -1) {
-            $sql .= "LIMIT " . $args['posts_per_page'] . "
-               OFFSET " . ($args['posts_per_page'] * ($args['paged'] - 1));
+            $sql .= "LIMIT %d OFFSET %d";
+
+            $placeholderValues = array_merge($placeholderValues, [
+                $args['posts_per_page'],
+                $args['posts_per_page'] * ($args['paged'] - 1)
+            ]);
         }
 
-        $results = self::dbResultTransform($wpdb->get_results($sql, ARRAY_A), $args);
+        $sql = $wpdb->prepare($sql, $placeholderValues);
+        $results = $wpdb->get_results($sql, ARRAY_A);
+        $results = self::dbResultTransform($results, $args);
 
-        if (!isset($args['ignore_cache']) || ! $args['ignore_cache']) {
+        if (!isset($args['ignore_cache']) || !$args['ignore_cache']) {
             Cache::set($args, $results);
         }
 
@@ -223,13 +240,16 @@ public static function delete($ids)
             return;
         }
 
-        global $wpdb;
+        if (!is_array($ids)) {
+            $ids = [$ids];
+        }
 
-        $ids = GeneralHelper::arrayToString($ids);
-        $ids = GeneralHelper::sanitiseForDbQuery($ids);
+        global $wpdb;
 
-        $wpdb->query("DELETE FROM " . $wpdb->prefix . GeneralHelper::$tableName . "
-                      WHERE id IN(" . $ids . ")");
+        $sql = "DELETE FROM " . $wpdb->prefix . GeneralHelper::$tableName . "
+                WHERE id IN(" . implode(',', array_fill(0, count($ids), '%d')) . ")";
+        $sql = $wpdb->prepare($sql, $ids);
+        $wpdb->query($sql);
     }
 
     public static function truncate()
@@ -258,11 +278,8 @@ public static function deleteOlderThan($timeInterval = null)
         $interval = $timeInterval == null ?  Settings::get('timescale') : $timeInterval;
         $timestamp = time() - $interval;
 
-        $sql = $wpdb->prepare(
-            "DELETE FROM " . $wpdb->prefix . GeneralHelper::$tableName . " WHERE time <= %d",
-            $timestamp
-        );
-
+        $sql = "DELETE FROM " . $wpdb->prefix . GeneralHelper::$tableName . " WHERE time <= %d";
+        $sql = $wpdb->prepare($sql, $timestamp);
         $wpdb->query($sql);
     }
 }
diff --git a/testing/tests/TestEmails.php b/testing/tests/TestEmails.php
index aa12d52..2c93330 100644
--- a/testing/tests/TestEmails.php
+++ b/testing/tests/TestEmails.php
@@ -5,156 +5,156 @@
 
 class TestEmails extends WP_UnitTestCase
 {
-	public function setUp(): void
-	{
-		Logs::truncate();
-	}
-
-	public function testMail()
-	{
-		$to = 'test@test.com';
-		$subject = 'subject';
-		$message = 'message';
-		$additionalHeaders = [GeneralHelper::$htmlEmailHeader, 'cc: test1@test.com'];
-
-		$imgAttachmentId = $this->factory()->attachment->create_upload_object(__DIR__ . '/../assets/img-attachment.png');
-		$pdfAttachmentId = $this->factory()->attachment->create_upload_object(__DIR__ . '/../assets/pdf-attachment.pdf');
-
-		wp_mail($to, $subject, $message, $additionalHeaders, [
-			get_attached_file($imgAttachmentId),
-			get_attached_file($pdfAttachmentId)
-		]);
-
-		$emailLogs = Logs::get();
-
-		$this->assertCount(1, $emailLogs);
-		$this->assertEquals($to, $emailLogs[0]['email_to']);
-		$this->assertEquals($subject, $emailLogs[0]['subject']);
-		$this->assertEquals($message, $emailLogs[0]['message']);
-		$this->assertTrue($emailLogs[0]['is_html']);
-
-		$this->assertEquals($additionalHeaders[0], $emailLogs[0]['additional_headers'][0]);
-		$this->assertEquals($additionalHeaders[1], $emailLogs[0]['additional_headers'][1]);
-
-		$this->assertEquals($imgAttachmentId, $emailLogs[0]['attachments'][0]['id']);
-		$this->assertEquals(wp_get_attachment_url($imgAttachmentId), $emailLogs[0]['attachments'][0]['url']);
-
-		$this->assertEquals($pdfAttachmentId, $emailLogs[0]['attachments'][1]['id']);
-		$this->assertEquals(wp_get_attachment_url($pdfAttachmentId), $emailLogs[0]['attachments'][1]['url']);
-
-		wp_delete_attachment($imgAttachmentId);
-		wp_delete_attachment($pdfAttachmentId);
-	}
-
-	public function testCorrectTos()
-	{
-		wp_mail('test@test.com', 'subject', 'message');
-		$this->assertTrue(Logs::getFirst()['status']);
-	}
-
-	public function testIncorrectTos()
-	{
-		wp_mail('testtest.com', 'subject', 'message');
-		$this->assertFalse(Logs::getFirst()['status']);
-	}
-
-	public function testHtmlEmailSetViaFilter()
-	{
-		$contentTypeFilterPriority = 999;
-		$updateContentType = function () {
-			return 'text/html';
-		};
-
-		add_filter('wp_mail_content_type', $updateContentType, $contentTypeFilterPriority);
-
-		// Send an email without explicitly setting the html header
-		wp_mail('test@test.com', 'subject', 'message');
-
-		remove_filter('wp_mail_content_type', $updateContentType, $contentTypeFilterPriority);
-
-		$this->assertTrue(Logs::get()[0]['is_html']);
-	}
-
-	public function testHtmlEmail()
-	{
-		// Test various formats
-		wp_mail('test@test.com', 'subject', 'message', [GeneralHelper::$htmlEmailHeader]);
-		wp_mail('test@test.com', 'subject', 'message', ['content-type:text/html']);
-		wp_mail('test@test.com', 'subject', 'message', ['Content-Type: text/html']);
-		wp_mail('test@test.com', 'subject', 'message', ['Content-Type: text/html;']);
-
-		$this->assertTrue(Logs::get()[0]['is_html']);
-		$this->assertTrue(Logs::get()[1]['is_html']);
-		$this->assertTrue(Logs::get()[2]['is_html']);
-		$this->assertTrue(Logs::get()[3]['is_html']);
-	}
-
-	public function testNonHtmlEmail()
-	{
-		wp_mail('test@test.com', 'subject', 'message');
-		$this->assertFalse(Logs::get()[0]['is_html']);
-	}
-
-	public function testWpFiltersWithMailCatcherAreUnchanged()
-	{
-		$originalTo = 'test@test.com';
-		$originalSubject = 'subject';
-		$originalMessage = 'message';
-		$originalContentType = 'multipart/alternative';
-		$originalAdditionalHeaders = ['content-type: ' . $originalContentType, 'cc: test1@test.com'];
-
-		$isWpMailFilterCalled = false;
-		$isWpMailContentFilterCalled = false;
-
-		$wpMailFilter = function ($args) use (&$isWpMailFilterCalled, $originalTo, $originalSubject, $originalMessage, $originalAdditionalHeaders) {
-			$isWpMailFilterCalled = true;
-
-			$this->assertEquals($args['to'], $originalTo);
-			$this->assertEquals($args['subject'], $originalSubject);
-			$this->assertEquals($args['message'], $originalMessage);
-
-			for ($i = 0; $i < count($args['headers']); $i++) {
-				$this->assertEquals($args['headers'][$i], $originalAdditionalHeaders[$i]);
-			}
-
-			return $args;
-		};
-
-		$wpMailContentFilter = function ($contentType) use (&$isWpMailContentFilterCalled, $originalContentType) {
-			$isWpMailContentFilterCalled = true;
-			$this->assertEquals($contentType, $originalContentType);
-			return $originalContentType;
-		};
-
-		// Add filters
-		add_filter('wp_mail', $wpMailFilter);
-		add_filter('wp_mail', $wpMailFilter, 9999991);
-		add_filter('wp_mail_content_type', $wpMailContentFilter);
-		add_filter('wp_mail_content_type', $wpMailContentFilter, 9999991);
-
-		// Send message
-		wp_mail($originalTo, $originalSubject, $originalMessage, $originalAdditionalHeaders);
-
-		// Assert filters were called
-		$this->assertTrue($isWpMailFilterCalled);
-		$this->assertTrue($isWpMailContentFilterCalled);
-
-		$emailLogs = Logs::get();
-
-		// Assert email was logged correctly
-		$this->assertCount(1, $emailLogs);
-		$this->assertEquals($originalTo, $emailLogs[0]['email_to']);
-		$this->assertEquals($originalSubject, $emailLogs[0]['subject']);
-		$this->assertEquals($originalMessage, $emailLogs[0]['message']);
-		$this->assertFalse($emailLogs[0]['is_html']);
-
-		$this->assertEquals($originalAdditionalHeaders[0], $emailLogs[0]['additional_headers'][0]);
-		$this->assertEquals($originalAdditionalHeaders[1], $emailLogs[0]['additional_headers'][1]);
-
-		// Tidy up
-		remove_filter('wp_mail', $wpMailFilter);
-		remove_filter('wp_mail', $wpMailFilter, 9999991);
-		remove_filter('wp_mail_content_type', $wpMailContentFilter);
-		remove_filter('wp_mail_content_type', $wpMailContentFilter, 9999991);
-	}
+    public function setUp(): void
+    {
+        Logs::truncate();
+    }
+
+    public function testMail()
+    {
+        $to = 'test@test.com';
+        $subject = 'subject';
+        $message = 'message';
+        $additionalHeaders = [GeneralHelper::$htmlEmailHeader, 'cc: test1@test.com'];
+
+        $imgAttachmentId = $this->factory()->attachment->create_upload_object(__DIR__ . '/../assets/img-attachment.png');
+        $pdfAttachmentId = $this->factory()->attachment->create_upload_object(__DIR__ . '/../assets/pdf-attachment.pdf');
+
+        wp_mail($to, $subject, $message, $additionalHeaders, [
+            get_attached_file($imgAttachmentId),
+            get_attached_file($pdfAttachmentId)
+        ]);
+
+        $emailLogs = Logs::get();
+
+        $this->assertCount(1, $emailLogs);
+        $this->assertEquals($to, $emailLogs[0]['email_to']);
+        $this->assertEquals($subject, $emailLogs[0]['subject']);
+        $this->assertEquals($message, $emailLogs[0]['message']);
+        $this->assertTrue($emailLogs[0]['is_html']);
+
+        $this->assertEquals($additionalHeaders[0], $emailLogs[0]['additional_headers'][0]);
+        $this->assertEquals($additionalHeaders[1], $emailLogs[0]['additional_headers'][1]);
+
+        $this->assertEquals($imgAttachmentId, $emailLogs[0]['attachments'][0]['id']);
+        $this->assertEquals(wp_get_attachment_url($imgAttachmentId), $emailLogs[0]['attachments'][0]['url']);
+
+        $this->assertEquals($pdfAttachmentId, $emailLogs[0]['attachments'][1]['id']);
+        $this->assertEquals(wp_get_attachment_url($pdfAttachmentId), $emailLogs[0]['attachments'][1]['url']);
+
+        wp_delete_attachment($imgAttachmentId);
+        wp_delete_attachment($pdfAttachmentId);
+    }
+
+    public function testCorrectTos()
+    {
+        wp_mail('test@test.com', 'subject', 'message');
+        $this->assertTrue(Logs::getFirst()['status']);
+    }
+
+    public function testIncorrectTos()
+    {
+        wp_mail('testtest.com', 'subject', 'message');
+        $this->assertFalse(Logs::getFirst()['status']);
+    }
+
+    public function testHtmlEmailSetViaFilter()
+    {
+        $contentTypeFilterPriority = 999;
+        $updateContentType = function () {
+            return 'text/html';
+        };
+
+        add_filter('wp_mail_content_type', $updateContentType, $contentTypeFilterPriority);
+
+        // Send an email without explicitly setting the html header
+        wp_mail('test@test.com', 'subject', 'message');
+
+        remove_filter('wp_mail_content_type', $updateContentType, $contentTypeFilterPriority);
+
+        $this->assertTrue(Logs::get()[0]['is_html']);
+    }
+
+    public function testHtmlEmail()
+    {
+        // Test various formats
+        wp_mail('test@test.com', 'subject', 'message', [GeneralHelper::$htmlEmailHeader]);
+        wp_mail('test@test.com', 'subject', 'message', ['content-type:text/html']);
+        wp_mail('test@test.com', 'subject', 'message', ['Content-Type: text/html']);
+        wp_mail('test@test.com', 'subject', 'message', ['Content-Type: text/html;']);
+
+        $this->assertTrue(Logs::get()[0]['is_html']);
+        $this->assertTrue(Logs::get()[1]['is_html']);
+        $this->assertTrue(Logs::get()[2]['is_html']);
+        $this->assertTrue(Logs::get()[3]['is_html']);
+    }
+
+    public function testNonHtmlEmail()
+    {
+        wp_mail('test@test.com', 'subject', 'message');
+        $this->assertFalse(Logs::get()[0]['is_html']);
+    }
+
+    public function testWpFiltersWithMailCatcherAreUnchanged()
+    {
+        $originalTo = 'test@test.com';
+        $originalSubject = 'subject';
+        $originalMessage = 'message';
+        $originalContentType = 'multipart/alternative';
+        $originalAdditionalHeaders = ['content-type: ' . $originalContentType, 'cc: test1@test.com'];
+
+        $isWpMailFilterCalled = false;
+        $isWpMailContentFilterCalled = false;
+
+        $wpMailFilter = function ($args) use (&$isWpMailFilterCalled, $originalTo, $originalSubject, $originalMessage, $originalAdditionalHeaders) {
+            $isWpMailFilterCalled = true;
+
+            $this->assertEquals($args['to'], $originalTo);
+            $this->assertEquals($args['subject'], $originalSubject);
+            $this->assertEquals($args['message'], $originalMessage);
+
+            for ($i = 0; $i < count($args['headers']); $i++) {
+                $this->assertEquals($args['headers'][$i], $originalAdditionalHeaders[$i]);
+            }
+
+            return $args;
+        };
+
+        $wpMailContentFilter = function ($contentType) use (&$isWpMailContentFilterCalled, $originalContentType) {
+            $isWpMailContentFilterCalled = true;
+            $this->assertEquals($contentType, $originalContentType);
+            return $originalContentType;
+        };
+
+        // Add filters
+        add_filter('wp_mail', $wpMailFilter);
+        add_filter('wp_mail', $wpMailFilter, 9999991);
+        add_filter('wp_mail_content_type', $wpMailContentFilter);
+        add_filter('wp_mail_content_type', $wpMailContentFilter, 9999991);
+
+        // Send message
+        wp_mail($originalTo, $originalSubject, $originalMessage, $originalAdditionalHeaders);
+
+        // Assert filters were called
+        $this->assertTrue($isWpMailFilterCalled);
+        $this->assertTrue($isWpMailContentFilterCalled);
+
+        $emailLogs = Logs::get();
+
+        // Assert email was logged correctly
+        $this->assertCount(1, $emailLogs);
+        $this->assertEquals($originalTo, $emailLogs[0]['email_to']);
+        $this->assertEquals($originalSubject, $emailLogs[0]['subject']);
+        $this->assertEquals($originalMessage, $emailLogs[0]['message']);
+        $this->assertFalse($emailLogs[0]['is_html']);
+
+        $this->assertEquals($originalAdditionalHeaders[0], $emailLogs[0]['additional_headers'][0]);
+        $this->assertEquals($originalAdditionalHeaders[1], $emailLogs[0]['additional_headers'][1]);
+
+        // Tidy up
+        remove_filter('wp_mail', $wpMailFilter);
+        remove_filter('wp_mail', $wpMailFilter, 9999991);
+        remove_filter('wp_mail_content_type', $wpMailContentFilter);
+        remove_filter('wp_mail_content_type', $wpMailContentFilter, 9999991);
+    }
 }
diff --git a/testing/tests/TestLogFunctions.php b/testing/tests/TestLogFunctions.php
index 5058d47..e296a36 100644
--- a/testing/tests/TestLogFunctions.php
+++ b/testing/tests/TestLogFunctions.php
@@ -11,6 +11,7 @@ class TestLogFunctions extends WP_UnitTestCase
 {
     public function setUp(): void
     {
+        parent::setUp();
         Logs::truncate();
     }
 
@@ -105,6 +106,53 @@ public function testCanResendMultipleMail()
         $this->assertEquals(count($mail), 4);
     }
 
+    public function testOrderLogsByTime()
+    {
+        $oldestSubject = 'I am the oldest';
+        $mostRecentSubject = 'I am the most recent';
+
+        wp_mail('test@test.com', $oldestSubject, 'message');
+        sleep(1);
+        wp_mail('test@test.com', $mostRecentSubject, 'message');
+
+        $log = Logs::getFirst([
+            'orderby' => 'time',
+            'order' => 'DESC'
+        ]);
+
+        $this->assertEquals($mostRecentSubject, $log['subject']);
+
+        $log = Logs::getFirst([
+            'orderby' => 'time',
+            'order' => 'ASC'
+        ]);
+
+        $this->assertEquals($oldestSubject, $log['subject']);
+    }
+
+    public function testOrderLogsBySubject()
+    {
+        $firstSubject = 'abc';
+        $secondSubject = 'def';
+
+        wp_mail('test@test.com', $firstSubject, 'message');
+        wp_mail('test@test.com', $secondSubject, 'message');
+
+        $log = Logs::getFirst([
+            'orderby' => 'subject',
+            'order' => 'ASC'
+        ]);
+
+        $this->assertEquals($firstSubject, $log['subject']);
+
+        $log = Logs::getFirst([
+            'orderby' => 'subject',
+            'order' => 'DESC'
+        ]);
+
+        $this->assertEquals($secondSubject, $log['subject']);
+    }
+
     public function testCanExportSingleLog()
     {
         $to = 'test@test.com';
diff --git a/testing/tests/TestSecurity.php b/testing/tests/TestSecurity.php
new file mode 100644
index 0000000..5cf7095
--- /dev/null
+++ b/testing/tests/TestSecurity.php
@@ -0,0 +1,47 @@
+alert("Hello");';
+        $escapedSubject = $mailTable->runHtmlSpecialChars($subjectBase);
+        $subject = $mailTable->column_subject(['subject' => $subjectBase]);
+
+        $this->assertEquals($subject, $escapedSubject);
+    }
+
+    public function testLogGetMethodIsImmuneToSqlInjection()
+    {
+        global $wpdb;
+        $originalWpDb = $wpdb;
+        $wpdb = Mockery::mock('wpdb');
+        $exploitedSql = "email_to+AND+(SELECT+7479+FROM+(SELECT(SLEEP(5)))UAKp)";
+
+        $wpdb->shouldIgnoreMissing()
+            ->shouldReceive('get_results')
+            ->withArgs(function ($sql) use ($exploitedSql) {
+                $doesSqlContainExploit = str_contains($sql, $exploitedSql);
+                $this->assertFalse($doesSqlContainExploit);
+                return true;
+            })
+            ->once()
+            ->andReturn([]);
+
+        Logs::get([
+            'orderby' => $exploitedSql
+        ]);
+
+        $wpdb = $originalWpDb;
+    }
+}